package dnbW.dbW;

import dnbW.uiW.dbW.DataSourceNodeObject;
import dnbW.uiW.dbW.DatabaseController;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Iterator;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;

/**
 *
 * @author zhouyong
 */
public class PathExpression {

    // path, for example: /a/b//c
    // from any (or context step itself) to the the context step
    // modifiable list, any step deletion, addition should happen
    // in this list
    private List<DefaultMutableTreeNode> path;
    // undifiable full path from document root to the context step
    // constructed in the constructor
    private List<DefaultMutableTreeNode> fullPath;
    // position predicate
    // default -1 (invalid)
    private int fromIndex = -1;
    private int toIndex = -1;
    // used to check if the node is leaf or attribute
    private boolean isAttribute = false;
    private boolean isLeaf = false;
    // context database, used for cross database join
    // immutable properties
    private DatabaseInfo contextDb;

    // <editor-fold desc="Data Source">
    private DataSource dataSource;

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    // </editor-fold>

    // <editor-fold desc="Property Change Support">
    private final PropertyChangeSupport propertyChangeSupport =
            new PropertyChangeSupport(this);

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }
    // </editor-fold>
    
    // construct a new path expression
    // step is the context step that will be used as a condition
    // contextDb is the data source of this path expression
    public PathExpression(
            DefaultMutableTreeNode step,
            DatabaseInfo contextDb) {
        this.contextDb = contextDb;
        path = this.constructPath(step);
        fullPath = Collections.unmodifiableList(path);

        isAttribute = isStepAttribute(step);
        isLeaf = isStepLeaf(step);
    }

    // <editor-fold desc="Supporting Private Methods">
    /**
     * Check whether a step is an attribute step
     * @param step
     * @return
     */
    private boolean isStepAttribute(DefaultMutableTreeNode step) {
        return step.getUserObject().toString().startsWith("@");
    }

    /**
     * Check whether a step is a leaf step
     */
    private boolean isStepLeaf(DefaultMutableTreeNode step) {
        // The step is a leaf step when it has no non-attribute substeps
        if (step.getChildCount() == 0) {
            return true;
        }
        else {
            for (Enumeration em = step.children(); em.hasMoreElements();) {
                DefaultMutableTreeNode subStep = (DefaultMutableTreeNode) em.nextElement();
                if (!isStepAttribute(subStep)) {
                    return false;
                }
            }
            return true;
        }
    }

    // </editor-fold>

    protected List<DefaultMutableTreeNode> constructPath(DefaultMutableTreeNode node) {
        List<DefaultMutableTreeNode> p = new ArrayList<DefaultMutableTreeNode>();

        for (TreeNode n : node.getPath()) {
            DefaultMutableTreeNode dmtn = (DefaultMutableTreeNode) n;
            if (dmtn.toString().equals(DatabaseInfo.SCHEMA_TREE_ROOT)) {
                continue;
            }

            if (DatabaseController.isDataSourceNode(dmtn)) {
                DataSourceNodeObject srcNode =
                        (DataSourceNodeObject) dmtn.getUserObject();
                this.dataSource = this.contextDb.getDataSource(srcNode.getSrcName());
                continue;
            }

            p.add(new DefaultMutableTreeNode(dmtn.getUserObject()));
        }

        for (int i = 1; i < p.size(); i++) {
            p.get(i - 1).add(p.get(i));
        }

        return p;
    }

    public List<DefaultMutableTreeNode> getPath() {
        return Collections.unmodifiableList(path);
    }

    public List<DefaultMutableTreeNode> getFullPath() {
        return fullPath;
    }

    /**
     * Gets the step of the path at a level
     * @param level Zero-based level of the step
     * @return      The step at the given level
     */
    public Object getStep(int level) {
        return this.fullPath.get(level).getUserObject();
    }

    public String getFullPathText() {
        StringBuilder result = new StringBuilder();
        for (DefaultMutableTreeNode step : this.getFullPath()) {
            result.append("/");
            result.append(step.toString());
        }
        return result.toString();
    }

    public int getFromIndex() {
        return fromIndex;
    }

    public void setFromIndex(int fromIndex) {
        this.fromIndex = fromIndex;
    }

    public int getToIndex() {
        return toIndex;
    }

    public void setToIndex(int toIndex) {
        this.toIndex = toIndex;
    }

    @Override
    public String toString() {
        return this.getFullPathText();
    }

    public DatabaseInfo getContextDb() {
        return contextDb;
    }

    public String getContextStep() {
        return path.get(path.size() - 1).toString();
    }

    public boolean isAttribute() {
        return this.isAttribute;
    }

    public boolean isLeaf() {
        return this.isLeaf;
    }

    public String getHumanReadableText() {
        String text =
                getFullPathText();
        if (getFromIndex() > 0) {
            text += "[" + getFromIndex() + "]";
        }
        return this.getDataSource().getName() + " : " + text;
    }

    public String getHumanReadableTextShort() {
        StringBuilder sb = new StringBuilder();
        sb.append(this.getDataSource().getName() + " : ");
        sb.append(this.getContextStep());
        return sb.toString();
    }

    /**
     * Finds the longest common prefix of two path expressions
     * @param p1    Path expression 1
     * @param p2    Path expression 2
     * @return      The zero-based index of the lowest common element
     */
    public static int lcp(PathExpression p1, PathExpression p2) {
        int i = 0;
        for (i = 0; i < p1.getFullPath().size() && i < p2.getFullPath().size(); i++) {
            DefaultMutableTreeNode n1 = p1.getFullPath().get(i);
            DefaultMutableTreeNode n2 = p2.getFullPath().get(i);

            if (!n1.toString().equals(n2.toString())) {
                break;
            }
        }
        return i - 1;
    }

    /**
     * Finds the longest common prefix of path expressions
     * @param pathExpressions   The path expressions
     * @return                  The zero-based index of the lowest common element
     */
    public static int lcp(PathExpression... pathExpressions) {
        int length = pathExpressions.length;
        if (length == 0) return -1;
        else if (length == 1) return pathExpressions[0].fullPath.size();
        else {
            Iterator[] fullPathIterators = new Iterator[length];
            for (int i = 0; i < length; i++) {
                fullPathIterators[i] = pathExpressions[i].fullPath.iterator();
            }

            int lengthCommonPrefix = 0;
            Object val, otherVal;
            boolean equal = true;
            while (true) {
                if (!fullPathIterators[0].hasNext()) break;
                val = ((DefaultMutableTreeNode)fullPathIterators[0].next()).getUserObject();
                for (int i = 1; i < length; i++) {
                    otherVal = ((DefaultMutableTreeNode)fullPathIterators[i].next()).getUserObject();
                    if (!fullPathIterators[i].hasNext() || !val.equals(otherVal)) {
                        equal = false;
                        break;
                    }
                }
                if (equal == true) lengthCommonPrefix++;
                else break;
            }
            return lengthCommonPrefix - 1;
        }
    }

    public boolean isSameDataSource(PathExpression pathExpression){
        return this.getDataSource().equals(pathExpression.getDataSource());
    }
}
